use std::collections::HashMap;

use super::client_proxy::ConnectionId;
use super::manager_proxy::{ClientConfig, MapInfo, NodeStatus, PlayerInfo, ScenarioInfo, TaskInfo};
use super::sensors_config::SensorsConfig;
use crate::{Error, Result};

#[derive(Debug)]
pub struct State {
    command_id: String,
    server_type: String,
    map: MapInfo,
    task: Option<TaskInfo>,
    scenario: Option<ScenarioInfo>,
    client_config: ClientConfig,
    players: HashMap<ConnectionId, Player>,
    vehicles: HashMap<String, ConnectionId>,
}

impl State {
    pub fn new(server_uri: String) -> Self {
        Self {
            command_id: String::new(),
            server_type: String::new(),
            map: MapInfo {
                name: String::new(),
                running_time: 0,
            },
            task: None,
            scenario: None,
            client_config: ClientConfig { server_uri },
            players: HashMap::new(),
            vehicles: HashMap::new(),
        }
    }

    pub fn set_running_time(&mut self, running_time: u64) {
        self.map.running_time = running_time;
        if let Some(scenario) = &mut self.scenario {
            scenario.running_time = running_time;
        }
    }

    pub fn set_command(&mut self, id: String) {
        self.command_id = id;
    }

    pub fn has_command(&self, id: &str) -> bool {
        self.command_id.as_str() == id
    }

    pub fn set_server_type(&mut self, server_type: String) {
        self.server_type = server_type;
    }

    pub fn set_task(&mut self, id: String, token: String) {
        self.task = Some(TaskInfo { id, token });
    }

    pub fn unset_task(&mut self) {
        self.task = None;
    }

    pub fn set_map_name(&mut self, map_name: String) {
        self.map.name = map_name;
    }

    pub fn has_map(&self, name: &str) -> bool {
        self.map.name.as_str() == name
    }

    pub fn set_scenario(&mut self, name: String) -> u64 {
        let id = self
            .scenario
            .as_ref()
            .map_or(0u64, |scenario| scenario.id + 1);

        self.scenario = Some(ScenarioInfo {
            id,
            name,
            running_time: 0,
            stoped: false,
            proc_success: false,
            success: false,
        });

        id
    }

    pub fn set_scenario_stop(&mut self, id: Option<u64>, proc_success: bool, success: bool) {
        if let Some(scenario) = &mut self.scenario {
            let need_set = if let Some(id) = id {
                id == scenario.id
            } else {
                true
            };

            if need_set {
                scenario.stoped = true;
                scenario.proc_success = proc_success;
                scenario.success = success;
            }
        }
    }

    pub fn has_scenario(&self, name: &str) -> bool {
        self.scenario
            .as_ref()
            .map_or(false, |scenario| scenario.name.as_str() == name)
    }

    pub fn get_scenario_result(&self) -> Option<bool> {
        self.scenario.as_ref().and_then(|scenario| {
            if scenario.stoped {
                Some(scenario.success)
            } else {
                None
            }
        })
    }

    pub fn add_player(
        &mut self,
        conn_id: ConnectionId,
        client_id: String,
        client_created: bool,
        vehicle_id: String,
        vehicle_type: String,
        sensors_config: SensorsConfig,
    ) -> Result<()> {
        if !self.players.contains_key(&conn_id) {
            self.players.insert(
                conn_id.clone(),
                Player {
                    client_id,
                    client_created,
                    vehicle_id: vehicle_id.clone(),
                    vehicle_type,
                    sensors_config,
                    sensors_times: SensorsTimes {
                        gnss_update_time: 0,
                        chassis_update_time: 0,
                        od_update_time: 0,
                    },
                },
            );

            if let Some(old_conn_id) = self.vehicles.insert(vehicle_id, conn_id) {
                self.players.remove(&old_conn_id);
            }

            Ok(())
        } else {
            Err(Error::new("Duplicated player".to_string()))
        }
    }

    pub fn get_player_info(
        &self,
        conn_id: &ConnectionId,
    ) -> Option<(String, bool, String, String)> {
        self.players.get(conn_id).map(|player| {
            (
                player.client_id.clone(),
                player.client_created,
                player.vehicle_id.clone(),
                player.vehicle_type.clone(),
            )
        })
    }

    pub fn get_player_with_vehicle_id(&self, vehicle_id: &str) -> Option<ConnectionId> {
        self.vehicles.get(&vehicle_id.to_string()).cloned()
    }

    pub fn update_player_sensors_time<'a>(
        &'a mut self,
        conn_id: &ConnectionId,
        update_time: u64,
    ) -> Option<(bool, bool, bool, &'a SensorsConfig)> {
        self.players.get_mut(conn_id).map(|player| {
            let update_gnss = update_time - player.sensors_times.gnss_update_time
                > (player.sensors_config.gnss.period * 1e6) as u64;
            if update_gnss {
                player.sensors_times.gnss_update_time = update_time;
            }

            let update_chassis = update_time - player.sensors_times.chassis_update_time
                > (player.sensors_config.chassis.period * 1e6) as u64;
            if update_chassis {
                player.sensors_times.chassis_update_time = update_time;
            }

            let update_od = update_time - player.sensors_times.od_update_time
                > (player.sensors_config.obstacles_detector.period * 1e6) as u64;
            if update_od {
                player.sensors_times.od_update_time = update_time;
            }

            (
                update_gnss,
                update_chassis,
                update_od,
                &player.sensors_config,
            )
        })
    }

    pub fn delete_player(&mut self, conn_id: &ConnectionId) {
        if let Some(player) = self.players.remove(conn_id) {
            self.vehicles.remove(&player.vehicle_id);
        }
    }

    pub fn delete_all_players(&mut self) {
        self.players.clear();
        self.vehicles.clear();
    }

    pub fn to_node_status(&self) -> NodeStatus {
        let players = self
            .players
            .iter()
            .map(|(_, player)| PlayerInfo {
                id: player.vehicle_id.clone(),
                vehicle_type: player.vehicle_type.clone(),
                client_created: player.client_created,
                client_id: player.client_id.clone(),
            })
            .collect();

        NodeStatus {
            command_id: self.command_id.clone(),
            server_type: self.server_type.clone(),
            task: self.task.clone(),
            scenario: self.scenario.clone(),
            map: self.map.clone(),
            client_config: self.client_config.clone(),
            players,
        }
    }
}

#[derive(Debug)]
pub struct Player {
    client_id: String,
    client_created: bool,
    vehicle_id: String,
    vehicle_type: String,
    sensors_config: SensorsConfig,
    sensors_times: SensorsTimes,
}

#[derive(Debug)]
struct SensorsTimes {
    gnss_update_time: u64,
    chassis_update_time: u64,
    od_update_time: u64,
}
